Odoo PSA Architecture
Objective: Define the architecture for a custom Asset Management module in Odoo 19 (Studio) that acts as the central hub for IT operations.
Core Concepts
- Asset Centricity: Every ticket, subscription line item, and security alert revolves around the
x_client_assetrecord. - Bi-Directional Sync: Odoo is the Master for Clients (Partners), Pulseway is the Master for Device Details.
- No-Code Constraint: Logic must be implemented using Odoo Studio Automated Actions (Python Server Actions) without custom backend modules.
Data Model: Client Asset (x_client_asset)
| Field Name | Type | Description |
|---|---|---|
x_name | Char | Computer Hostname (Primary Key) |
x_serial_number | Char | BIOS Serial Number |
x_partner_id | Many2one | Link to res.partner (Company) |
x_assigned_user_id | Many2one | Link to res.partner (Contact/Person) |
x_pulseway_id | Char | Unique System Identifier from Pulseway |
x_os_info | Char | Operating System Version |
x_last_audit | Datetime | Last sync timestamp |
x_subscription_active | Boolean | Computed field for billing |
Odoo Studio Implementation Steps (T008, T013)
- Create Model: Open Studio -> New App "PSA" -> New Model "Client Asset" (
x_client_asset). - Add Fields: Drag and drop fields corresponding to the Data Model above.
- Set
x_nameas the "Rec Name". - Set
x_pulseway_idas indexed (for faster lookups during sync).
- Set
- Views:
- List View: Show Name, Partner, OS, Last Audit.
- Form View: Group details, add "Helpdesk Tickets" smart button (One2many relation).
- Helpdesk Integration:
- Open "Helpdesk" App -> Open Ticket Form -> Studio.
- Add Many2one field
x_asset_idpointing tox_client_asset. - Add domain filter to
x_asset_id:[('x_partner_id', '=', partner_id)](Only show assets belonging to the ticket's customer).
Server Action Logic (T010, T011, T015)
1. Webhook Handler: System Registered & Alerts (Python)
Trigger: Incoming Webhook (URL called by Pulseway/RocketCyber)
# T010 & T011 Combined Logic
payload = request.jsonrequest
event_type = payload.get('EventType') # Pulseway Header or Payload field
if event_type == 'SystemRegistered':
# --- Sync Asset Logic ---
sys_id = payload.get('SystemIdentifier')
asset = env['x_client_asset'].search([('x_pulseway_id', '=', sys_id)], limit=1)
vals = {
'x_name': payload.get('Name'),
'x_os_info': payload.get('OS'),
'x_serial_number': payload.get('CustomFields', {}).get('Serial'),
'x_last_audit': datetime.now(),
}
if asset:
asset.write(vals)
else:
# Link to Partner based on Group Name
group_name = payload.get('Group')
partner = env['res.partner'].search([('name', '=', group_name)], limit=1)
if not partner:
# Fallback: Log warning or create dummy partner?
# For now: Do not create asset if partner not found to ensure data integrity
log("Partner not found for group: " + group_name)
else:
vals['x_pulseway_id'] = sys_id
vals['x_partner_id'] = partner.id
env['x_client_asset'].create(vals)
elif event_type == 'Notification':
# --- Alert to Ticket Logic ---
sys_id = payload.get('SystemIdentifier')
asset = env['x_client_asset'].search([('x_pulseway_id', '=', sys_id)], limit=1)
if asset:
ticket_vals = {
'name': f"Alert: {payload.get('Header')}",
'description': payload.get('Message'),
'partner_id': asset.x_partner_id.id,
'x_asset_id': asset.id,
'priority': '2' if 'Critical' in payload.get('Priority', '') else '1',
'team_id': env.ref('helpdesk.support_team').id
}
env['helpdesk.ticket'].create(ticket_vals)
2. Downstream Sync: Create Client in Pulseway (T012)
Trigger: On Creation of res.partner (Automated Action)
# T012 Logic
if record.is_company: # Only for parent companies
import requests
import json
url = "https://api.pulseway.com/v2/organizations"
headers = {
"Authorization": "Bearer " + env['ir.config_parameter'].get_param('pulseway.api_key'),
"Content-Type": "application/json"
}
data = {
"Name": record.name,
"Description": "Synced from Odoo"
}
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
except Exception as e:
# Log error to Chatter
record.message_post(body=f"Failed to sync to Pulseway: {str(e)}")
3. Billing Sync: Update Subscriptions (T015, T016)
Trigger: Scheduled Action (e.g., Monthly on the 15th)
# T015 Logic
managed_seat_product_id = env.ref('product.managed_seat_product').id # Replace with actual XML ID or search
subscriptions = env['sale.order'].search([
('state', '=', 'sale'),
('is_subscription', '=', True)
])
for sub in subscriptions:
partner = sub.partner_id
# Count active assets
asset_count = env['x_client_asset'].search_count([
('x_partner_id', '=', partner.id),
('x_status', '=', 'active') # T016: Handle decommissioned assets
])
# Update or Create Line Item
line = sub.order_line.filtered(lambda l: l.product_id.id == managed_seat_product_id)
if line:
line.write({'product_uom_qty': asset_count})
sub.message_post(body=f"Auto-updated Asset count to {asset_count}")
elif asset_count > 0:
# Create line if missing but assets exist
env['sale.order.line'].create({
'order_id': sub.id,
'product_id': managed_seat_product_id,
'product_uom_qty': asset_count
})